package com.jsftoolkit.gen.config;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;

import javax.faces.component.UIComponent;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPathExpressionException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import com.jsftoolkit.gen.config.impl.FacesConfigEditorImpl;
import com.jsftoolkit.gen.config.impl.TaglibEditorImpl;
import com.jsftoolkit.gen.config.impl.TldEditorImpl;
import com.jsftoolkit.gen.info.ComponentInfo;
import com.jsftoolkit.gen.info.PropertyInfo;
import com.jsftoolkit.utils.DeferedFileOutputStream;
import com.jsftoolkit.utils.Utils;

/**
 * This class updates configuration files as necessary to register a component.
 * <p>
 * This class is safe for concurrent usage by multiple threads, however, as one
 * might expect, concurrent updates to the same configuration file will have an
 * undefined result.
 * 
 * @author noah
 * 
 */
public class ConfigurationUpdater {

	private static final String FACELET_SYSTEM = "http://java.sun.com/dtd/facelet-taglib_1_0.dtd";

	private static final String FACELET_PUBLIC = "-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN";

	private DocumentBuilder documentBuilder;

	private TldEditor tldEditor;

	private FacesConfigEditor facesConfigEditor;

	private TaglibEditor taglibEditor;

	public ConfigurationUpdater() throws ParserConfigurationException,
			XPathExpressionException {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

		documentBuilder = factory.newDocumentBuilder();

		documentBuilder.setEntityResolver(new EntityResolver() {
			public InputSource resolveEntity(String publicId, String systemId) {
				// grrr, facelets...
				if (FACELET_PUBLIC.equals(publicId)
						|| FACELET_SYSTEM.equals(systemId)) {
					return new InputSource(getClass().getResourceAsStream(
							"/com/jsftoolkit/gen/facelet-taglib_1_0.dtd"));
				}
				return null;
			}
		});

		tldEditor = new TldEditorImpl();
		facesConfigEditor = new FacesConfigEditorImpl();
		taglibEditor = new TaglibEditorImpl();
	}

	/**
	 * Updates each relevant configuration file for the given component. If the
	 * file does not exist, it will be created.
	 * 
	 * @param info
	 */
	public void updateAll(ComponentInfo info) throws SAXException, IOException,
			XPathExpressionException, IntrospectionException,
			TransformerException {
		// try to create each file (if necessary) and update it for the
		// component
		String facesConfig = info.getConfig().getFacesConfig();
		if (null != facesConfig) {
			File file = new File(facesConfig);
			System.out.println("Updating faces-config " + file);
			updateFacesConfig(file, info);
		}

		String taglibXml = info.getConfig().getTaglibXml();
		String namespace = info.getConfig().getNamespace();
		if (null != taglibXml) {
			File file = new File(taglibXml);
			System.out.println("Updating taglib.xml  " + file);
			updateTaglibXml(namespace, file, info);
		}

		String tldFile = info.getConfig().getTldFile();
		if (null != tldFile) {
			String shortName = info.getConfig().getLibraryShortName();
			File file = new File(tldFile);
			System.out.println("Updating TLD " + file);
			updateTld(namespace, shortName, file, info);
		}
	}

	/**
	 * Parses faces-config.xml from in, adds the given components and writes the
	 * updated file to out.
	 * 
	 * @param in
	 * @param out
	 * @param components
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws IOException
	 * @throws XPathExpressionException
	 * @throws TransformerException
	 */
	public void updateFacesConfig(InputStream in, OutputStream out,
			ComponentInfo... components) throws SAXException, IOException,
			XPathExpressionException, TransformerException {
		Utils.write(
				domUpdateFacesConfig(documentBuilder.parse(in), components),
				out);
	}

	public void updateFacesConfig(File facesConfig, ComponentInfo... infos)
			throws XPathExpressionException, FileNotFoundException,
			SAXException, IOException, TransformerException {
		System.out.println("Updating faces-config " + facesConfig);
		if (!facesConfig.exists() && Utils.createFileParents(facesConfig)) {
			createFacesConfig(facesConfig);
		}
		updateFacesConfig(new FileInputStream(facesConfig),
				new DeferedFileOutputStream(facesConfig), infos);
	}

	/**
	 * 
	 * @param facesConfig
	 *            the parsed faces-config.xml
	 * @param components
	 *            the components to add
	 * @return
	 * @throws XPathExpressionException
	 */
	public Document domUpdateFacesConfig(Document facesConfig,
			ComponentInfo... components) throws XPathExpressionException {
		for (ComponentInfo info : components) {

			// prepare the variables
			String registerClass = info.getConfig().getRegisterClass();
			// if register class is empty, don't do it
			if (!Utils.isEmpty(registerClass)) {
				System.out.println("Registering component class: "
						+ registerClass);
				facesConfigEditor.registerComponent(facesConfig,
						info.getType(), registerClass);
			}

			String registerRenderer = info.getConfig().getRegisterRenderer();
			// if registerRenderer is empty, do nothing
			if (!Utils.isEmpty(registerRenderer)) {
				System.out.println("Registering renderer: " + registerRenderer);

				facesConfigEditor.registerRenderer(facesConfig, info
						.getFamily(), info.getRendererType(), registerRenderer,
						null);
			}
		}
		return facesConfig;
	}

	public void updateTaglibXml(String namespace, File file,
			ComponentInfo... infos) throws XPathExpressionException,
			FileNotFoundException, SAXException, IOException,
			TransformerException {
		System.out.println("Updating taglib.xml " + file);
		if (!file.exists() && Utils.createFileParents(file)) {
			createFaceletsTaglib(file, namespace);
		}
		updateTaglibXml(new FileInputStream(file), new DeferedFileOutputStream(
				file), infos);
	}

	/**
	 * Updates the .taglib.xml file for the given components.
	 * 
	 * @param in
	 * @param out
	 * @param components
	 * @throws SAXException
	 * @throws IOException
	 * @throws XPathExpressionException
	 * @throws TransformerException
	 */
	public void updateTaglibXml(InputStream in, OutputStream out,
			ComponentInfo... components) throws SAXException, IOException,
			XPathExpressionException, TransformerException {
		Document doc = domUpdateTaglibXml(documentBuilder.parse(in), components);
		Transformer serializer = Utils.getDefaultXmlSerializer();
		// set the doctype
		serializer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, FACELET_PUBLIC);
		serializer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, FACELET_SYSTEM);
		// the facelets taglibs written by Jacob Hookum omit the declaration, so
		// we will too
		serializer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
		Utils.write(doc, serializer, out);
	}

	/**
	 * Updates the .taglib.xml file for the given components.
	 * 
	 * @param taglib
	 * @param components
	 * @return
	 * @throws XPathExpressionException
	 */
	public Document domUpdateTaglibXml(Document taglib,
			ComponentInfo... components) throws XPathExpressionException {
		for (ComponentInfo info : components) {
			taglibEditor.registerTag(taglib, info.getConfig().getTagName(),
					info.getType(), info.getRendererType());
		}
		return taglib;
	}

	/**
	 * Updates the TLD for the given components.
	 * 
	 * @param in
	 * @param out
	 * @param components
	 * @throws SAXException
	 * @throws IOException
	 * @throws XPathExpressionException
	 * @throws IntrospectionException
	 * @throws TransformerException
	 */
	public void updateTld(InputStream in, OutputStream out,
			ComponentInfo... components) throws SAXException, IOException,
			XPathExpressionException, IntrospectionException,
			TransformerException {
		Utils.write(domUpdateTld(documentBuilder.parse(in), components), out);
	}

	public void updateTld(String namespace, String shortName, File file,
			ComponentInfo... components) throws SAXException, IOException,
			XPathExpressionException, IntrospectionException,
			TransformerException {
		System.out.println("Updating TLD " + file);
		if (!file.exists() && Utils.createFileParents(file)) {
			createTld(new DeferedFileOutputStream(file), namespace, shortName);
		}
		updateTld(new FileInputStream(file), new DeferedFileOutputStream(file),
				components);
	}

	/**
	 * Updates the TLD for the given components.
	 * 
	 * @param taglib
	 * @param components
	 * @return
	 * @throws XPathExpressionException
	 * @throws IntrospectionException
	 */
	public Document domUpdateTld(Document taglib, ComponentInfo... components)
			throws XPathExpressionException, IntrospectionException {
		for (ComponentInfo info : components) {
			Element tag = tldEditor.registerTag(taglib, info.getConfig()
					.getTagName(), info.getTag().getCanonicalClassName());
			// We have to generate a whole attribute element for every
			// single component and renderer attribute.
			// There is no inheritance in TLD files. JSP sucks.

			// get the attributes from the tag super-class
			for (PropertyDescriptor pd : info.getTag().getPropertyDescriptors()) {
				// we assume here that all the inherited properties are
				// strings... If this is not the case, a PropertyInfo instance
				// should be provided (see below) which will overwrite the
				// string type
				tldEditor.registerAttribute(tag, pd.getName(), String.class,
						false);
			}

			// write the attributes that only apply to the renderer
			for (String attrib : info.getRenderer().getAttribs()) {
				tldEditor.registerAttribute(tag, attrib, String.class, false);
			}

			// write all the properties from ComponentInfo
			for (PropertyInfo pinfo : info.getProperties().values()) {
				tldEditor.registerAttribute(tag, pinfo.getName(), Utils
						.getValue(pinfo.getType(), String.class), pinfo
						.isRequired());
			}

			// write the standard properties (binding, id & rendered)
			tldEditor.registerAttribute(tag, "binding", UIComponent.class,
					false);
			tldEditor.registerAttribute(tag, "id", null, false);
			tldEditor.registerAttribute(tag, "rendered", boolean.class, false);
		}
		return taglib;
	}

	/**
	 * Writes an empty faces-config.xml to the given stream.
	 * 
	 * @param out
	 * @throws IOException
	 * @throws IllegalAccessException
	 * @throws InstantiationException
	 * @throws ClassNotFoundException
	 * @throws IOException
	 */
	public void createFacesConfig(File file) throws IOException {

		// faces config must have the DOCTYPE or some implementations will barf
		PrintStream out = new PrintStream(file);
		out
				.print("<?xml version=\"1.0\"?>\n"
						+ "<!DOCTYPE faces-config PUBLIC\n"
						+ "  \"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN\""
						+ "\n  \"http://java.sun.com/dtd/web-facesconfig_1_0.dtd\">\n"
						+ "<faces-config/>\n");
		out.close();
	}

	/**
	 * Creates an empty facelets taglib for the given namespace.
	 * 
	 * @param out
	 * @param namespace
	 * @throws IOException
	 */
	public void createFaceletsTaglib(File file, String namespace)
			throws IOException {
		PrintStream pOut = new PrintStream(file);
		// stupid taglib.xml *requires* a DOCTYPE, but DOM wont let us add
		// one, so we need to prewrite the file with the doctype.
		pOut.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
				+ "<!DOCTYPE facelet-taglib PUBLIC "
				+ "\"-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN\" "
				+ "\"http://java.sun.com/dtd/facelet-taglib_1_0.dtd\">\n"
				+ "<facelet-taglib>\n" + "    <namespace>" + namespace
				+ "</namespace>\n" + "</facelet-taglib>\n");
		pOut.close();
	}

	/**
	 * Creates and empty TLD file.
	 * 
	 * @param out
	 * @param uri
	 * @param shortName
	 * @throws IOException
	 * @throws IllegalAccessException
	 * @throws InstantiationException
	 * @throws ClassNotFoundException
	 * @throws ClassCastException
	 * @throws IOException
	 */
	public void createTld(OutputStream out, String uri, String shortName) {
		// stupid DOCTYPEs...
		PrintStream ps = new PrintStream(out);
		ps.println("<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>");
		ps
				.println("<taglib xmlns=\"http://java.sun.com/xml/ns/javaee\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
		ps
				.println("  xsi:schemaLocation=\"http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd\"");
		ps.println("  version=\"2.1\">");
		ps.println("  <tlib-version>1.0</tlib-version>");
		ps.printf("  <short-name>%s</short-name>\n", shortName);
		ps.printf("  <uri>%s</uri>\n", uri);
		ps.println("</taglib>");
		ps.close();
	}

	public DocumentBuilder getDocumentBuilder() {
		return documentBuilder;
	}

	public void setDocumentBuilder(DocumentBuilder documentBuilder) {
		this.documentBuilder = documentBuilder;
	}

	public TldEditor getTldEditor() {
		return tldEditor;
	}

	public void setTldEditor(TldEditor tldEditor) {
		this.tldEditor = tldEditor;
	}

	public FacesConfigEditor getFacesConfigEditor() {
		return facesConfigEditor;
	}

	public void setFacesConfigEditor(FacesConfigEditor facesConfigEditor) {
		this.facesConfigEditor = facesConfigEditor;
	}

	public TaglibEditor getTaglibEditor() {
		return taglibEditor;
	}

	public void setTaglibEditor(TaglibEditor taglibEditor) {
		this.taglibEditor = taglibEditor;
	}

}
